package br.edu.ufcg.msnlab2.grupo07.monteCarlo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import mathExpr.ExpressionConfiguration;
import mathExpr.UnknownDefinitionException;
import mathExpr.evaluator.realEvaluator.Real;
import mathExpr.evaluator.realEvaluator.RealType;

import org.apache.commons.math.random.RandomDataImpl;

import br.edu.ufcg.msnlab2.MSNLabException;
import br.edu.ufcg.msnlab2.misc.Function;


/**
 * This class contains the common operations needed for Monte Carlo
 * algorithms implementation.
 * @author David Candeia
 * @author Felipe Leal
 *
 */
public class MonteCarloSolver {

	public static long SEED = 19580427;

	/**
	 * This method is responsible for calculating the volume of a certain
	 * set of variable limits
	 * @param limits The limits for each variable in the function
	 * @return The volume according to the variable limits
	 */
	public double calculateVolume(Map<String, Tuple<Double, Double>> limits) {
		double volume = 1d;
		for(Entry<String, Tuple<Double, Double>> entry : limits.entrySet()){
			volume *= (entry.getValue().getA() - entry.getValue().getK());
		}
		return volume;
	}
	
	/**
	 * This method is responsible for calculating the variance in the function values given a set of sampled
	 * points for the variables.
	 * @param function The function expression
	 * @param sampledPoints The set of sampled points
	 * @param pointsAverage The average of the function values for the sampled points
	 * @param numberOfPoints 
	 * @return The variance in the function values
	 */
	public double calculateFunctionVariance(Function function, Map<String, List<Double>> sampledPoints, double pointsAverage, long numberOfPoints){
		long maxIndex = numberOfPoints;
		int index = 0;
		double totalSum = 0d;
		
//		function.setType(RealType.TYPE);
		
		for(int i = index; i < maxIndex; i++){
//			for(Entry<String, List<Double>> entry : sampledPoints.entrySet()){
//				double value = entry.getValue().get(i);
//				function.defineVariable(entry.getKey(), new Real(value), RealType.TYPE);
//			}
			
			//Calculating function value for a point
//			Real result = (Real)function.evaluateExpression();
			double result = function.evaluate(sampledPoints, i);
//			totalSum += Math.pow(result.getValue() - pointsAverage, 2);
			totalSum += Math.pow(result - pointsAverage, 2);
		}
		
		//Calculating function variance 
		double functionVariance = totalSum / numberOfPoints;
		return functionVariance;
	}
	
	/**
	 * This method calculates the variance in the integral estimative
	 * calculated by the Monte Carlo method.
	 * @param function The function expression
	 * @param sampledPoints A map containing the value of each variable for each sampled point
	 * @param pointsAverage The mean value of the function evaluated in each sampled point
	 * @param volume The volume calculated with the limits for each variable
	 * @param numberOfPoints The number of sampled points
	 * @return The variance of the integral estimative
	 * @throws MSNLabException 
	 */
	public double calculateVariance(Function function, Map<String, List<Double>> sampledPoints, double pointsAverage, double volume, long numberOfPoints) throws MSNLabException {
		
		if(sampledPoints.size() == 0){
			throw new RuntimeException("Invalid set of sampled points!");
		}
		
		for(String variable : sampledPoints.keySet()){
			if(sampledPoints.get(variable).size() == 0){
				throw new RuntimeException("Invalid set of sampled points!");
			}
		}
		
		if(numberOfPoints <= 0){
			throw new MSNLabException("Invalid number of points!");
		}
		
		if(function == null){
			throw new RuntimeException("Invalid function!");
		}
		
		if(volume < 0){
			throw new MSNLabException("Negative interval is not allowed!");
		}
		
		if(volume == 0){
			throw new MSNLabException("Integral is not determined for a point!");
		}
		
		int index = 0;
		long maxIndex = numberOfPoints;
		double totalSum = 0d;
		
//		function.setType(RealType.TYPE);
		
		for(int i = index; i < maxIndex; i++){
//			for(Entry<String, List<Double>> entry : sampledPoints.entrySet()){
//				double value = entry.getValue().get(i);
//				function.defineVariable(entry.getKey(), new Real(value), RealType.TYPE);
//			}
			
			//Calculating function value for a point
//			Real result = (Real)function.evaluateExpression();
			double result = function.evaluate(sampledPoints, i);
//			totalSum += Math.pow(result.getValue() - pointsAverage, 2);
			totalSum += Math.pow(result - pointsAverage, 2);
		}
		
		//Calculating function variance 
		double functionVariance = calculateFunctionVariance(function, sampledPoints, pointsAverage, numberOfPoints);
//		double functionVariance2 = totalSum / numberOfPoints;
		
		//Calculating integral variance
		double integralVariance = Math.pow(volume, 2)*functionVariance / numberOfPoints;
		
		return integralVariance;
	}

	/**
	 * This method calculates the sum of the function values in each point
	 * sampled earlier.
	 * @param sampledPoints A map containing the value of each variable for each sampled point
	 * @param function	The function expression	
	 * @param numberOfPoints The number of points sampled
	 * @return The sum of function values in each sampled point
	 * @throws MSNLabException 
	 */
	public double calculateFunctionValueInEachPoint(Map<String, List<Double>> sampledPoints, Function function, long numberOfPoints) throws MSNLabException {
		
		if(sampledPoints.size() == 0){
			throw new RuntimeException("Invalid set of sampled points!");
		}
		
		for(String variable : sampledPoints.keySet()){
			if(sampledPoints.get(variable).size() == 0){
				throw new RuntimeException("Invalid set of sampled points!");
			}
		}
		
		if(numberOfPoints <= 0){
			throw new MSNLabException("Invalid number of points!");
		}
		
		if(function == null){
			throw new RuntimeException("Invalid function!");
		}
		
		try{
			int index = 0;
			long maxIndex = numberOfPoints;
			double totalValue = 0d;
			
			for(int i = index; i < maxIndex; i++){
				
				//Calculating function value for a point
//				Real result = calculateFunctionValueForOnePoint(sampledPoints, function, i); 
				double result = function.evaluate(sampledPoints, i);
//				totalValue += result.getValue();
				totalValue += result;
			}
			
			return totalValue;
		}catch(UnknownDefinitionException e){
			throw new RuntimeException(e.getMessage());
		}
	}
	
	
	/**
	 * This method will sample some points according to a uniform distribution
	 * for each variable v that is part of the function expression and 
	 * according to the integral limits.
	 * @param limits The limits of the integral for each variable
	 * @param numberOfPoints The number of points to be sampled
	 * @return A map containing, for each variable, the whole set of sampled points
	 * @throws MSNLabException 
	 */
	public Map<String, List<Double>> samplePoints(Map<String, Tuple<Double, Double>> limits, long numberOfPoints) throws MSNLabException {
		if(numberOfPoints <= 0){
			throw new MSNLabException("Invalid number of points to estimate integral with Monte Carlo!");
		}
		
		if(limits == null){
			throw new RuntimeException("Invalid list of limits!");
		}
		
		Map<String, List<Double>> sampledPoints = new HashMap<String, List<Double>>();
		RandomDataImpl random = new RandomDataImpl();
		random.reSeed(SEED);
		
//		System.out.println("MENOS "+limits.get("x").getA()+" "+limits.get("x").getK());
//		System.out.println("SAMPLE: "+limits);
		for(int i =0; i < numberOfPoints; i++){
			for(Entry<String, Tuple<Double, Double>> entry : limits.entrySet()){
				double min = ((Tuple<Double, Double>)entry.getValue()).getK();
				double max = ((Tuple<Double, Double>)entry.getValue()).getA();
				
//				Random generator = new Random(SEED);
//				double value = generator.nextDouble() * max + min;
				double value = 0d;
				if(min == max){
					value = min;
				}else{
//					System.out.println(min+" "+max);
					value = random.nextUniform(min, max);
//					System.out.println("DEPOIS");
				}
				
				List<Double> sampleValues = sampledPoints.get(entry.getKey());
				if(sampleValues == null){
					sampleValues = new ArrayList<Double>();
					sampledPoints.put(entry.getKey(), sampleValues);
				}
				
				sampleValues.add(value);
			}
		}
		
		return sampledPoints;
	}
}
